package com.db4o.odbgen.plugins.java.maingenerator;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;


import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;

import com.db4o.internal.odbgen.StringUtils;
import com.db4o.internal.odbgen.TemplatesUtils;
import com.db4o.internal.odbgen.plugins.TemplateList;
import com.db4o.odbgen.OdbgenException;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.DataType;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.PrimitiveType;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSClass;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSField;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSInterface;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSMethod;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSMethodParameter;
import com.db4o.odbgen.plugins.java.maingenerator.typedxmlschema.TSModifier;

/**
 * Supports various methods that will be generated alongside XmlSchema source files.
 * The following classes/methods are supported.
 * <ol>
 * <li>
 * Class Db4oUtils. Contains methods used to configure ObjectContainter and methods for all
 * external callbacks.
 * Follow the methods.
 * <ol>
 * <li>
 * public static void applyCallbacksClient(ObjectContainer container)
 * <p> Applies needed callbacks to the supplied ObjectContainer.
 * This is used only when working on client side in networked mode.
 * Use this.getApplyCallbacks() to add content to this method.
 * </li>
 * <li>
 * public static void applyCallbacksServer(ObjectServer server)
 * <p> Applies needed callbacks to the supplied ObjectServer.
 * This is used only when working on server side in networked mode.
 * Use this.getApplyCallbacks() to add content to this method.
 * </li>
 * <li>
 * public static void applyCallbacksEmbedded(ObjectContainer container)
 * <p> Applies needed callbacks to the supplied ObjectContainer.
 * This is used only when working in embedded mode.
 * Use this.getApplyCallbacks() to add content to this method.
 * </li>
 * <li>
 * public static void applyConfigClient(Configuration config)
 * <p> Apply needed configuration to the supplied db4o configuration object.
 * This is used only when working on the client side in networked mode.
 * Use this.getConfig() to add content to this method.
 * </li>
 * <li>
 * public static void applyConfigServer(Configuration config)
 * <p> Apply needed configuration to the supplied db4o configuration object.
 * This is used only when working on the server side in networked mode.
 * Use this.getConfig() to add content to this method.
 * </li>
 * <li>
 * public static void applyConfigEmbedded(Configuration config)
 * <p> Apply needed configuration to the supplied db4o configuration object.
 * This is used only when working in embedded mode.
 * Use this.getConfig() to add content to this method.
 * </li>
 * <li>
 * public static void onStdCallbackConfig(Event4 e, EventArgs args)
 * <p> This represent a set of methods generated for each external callback type supported
 * by db4o. Each callback type is generated three times: for server, client and embedded configs.
 * There are exceptions where the callbacks are not generated for the client config.
 * See {@link ExternalCallbackType}.ServerCallback() for details on this.
 * Use this.getStandardCallback() in order to add content to the callbacks.
 * </li>
 * </ol>
 * </ol>
 * @author liviug
 */
public class XmlSchemaUtils{
	private final String odb4UtilsClassName = "Db4oUtils";

	private MainGenerator _mainGenerator;
	private TSClass _odb4UtilsClass; 
	private TSMethod _applyCallbacksClientMethod; 
	private TSMethod _applyCallbacksServerMethod; 
	private TSMethod _applyCallbacksEmbeddedMethod; 
	private TSMethod _applyConfigClientMethod; 
	private TSMethod _applyConfigServerMethod; 
	private TSMethod _applyConfigEmbeddedMethod; 
	/**
	 * Holds all standard callbacks.
	 * The key is the callback name as returned by this.getStandardCallbackMethodName().
	 */
	Map<String, TSMethod> _standardCallbacks = new Hashtable<String, TSMethod>();

	/**
	 * Defines the scenarios used to initialize an ObjectContainer or a db4o Configuration. 
	 * @author liviug
	 */
	public static enum Scenario{
		/**
		 * Used to initialize an ObjectServer.
		 * Applied only in networked mode on the server side.
		 */
		SERVER("applyConfigServer", "applyCallbacksServer"),
		/**
		 * Used to initialize an ObjectContainer.
		 * Applied only in networked mode on the client side.
		 */
		CLIENT("applyConfigClient", "applyCallbacksClient"),
		/**
		 * Used to initialize an ObjectContainer.
		 * Applied only in embedded mode.
		 */
		EMBEDDED("applyConfigEmbedded", "applyCallbacksEmbedded");
		
		private String _applyConfigMethodName;
		private String _applyCallbacksMethodName;
		
		private Scenario(String applyConfigMethodName, String applyCallbacksMethodName){
			this._applyConfigMethodName = applyConfigMethodName;
			this._applyCallbacksMethodName = applyCallbacksMethodName;
		}
		
		public String getApplyConfigMethodName(){
			return this._applyConfigMethodName;
		}
		public String getApplyCallbacksMethodName(){
			return this._applyCallbacksMethodName;
		}
	}


	/**
	 * Defines all types of external callbacks.
	 * @author liviug
	 */
	public static enum ExternalCallbackType{
		QUERYSTARTED    (false,  true, "queryStarted"),
		QUERYFINISHED   (false,  true, "queryFinished"),
		CREATING        (false,  true, "creating"),  
		CREATED         (false,  true, "created"),  
		ACTIVATING      (false,  true, "activating"),
		ACTIVATED       (false,  true, "activated     "), 
		DEACTIVATING    (false,  true, "deactivating"),
		DEACTIVATED     (false,  true, "deactivated"),
		UPDATING        (false,  true, "updating"),  
		UPDATED         (false,  true, "updated"),  
		DELETING        (true, false,  "deleting"),  
		DELETED         (true, false,  "deleted"),  
		COMMITTING      (true, false,  "committing"),
		COMMITTED       (false,  true, "committed"); 
		
		private boolean _isServerCallback;
		private boolean _isClientCallback;
		private String _name;

		private ExternalCallbackType(boolean serverCallback, boolean clientCallback, String name){
			this._isServerCallback = serverCallback;
			this._isClientCallback = clientCallback;
			this._name = name;
		}
		
		/**
		 * Returns true if this callback must be used only on server side.
		 */
		public boolean isServerCallback(){
			return this._isServerCallback;
		}

		/**
		 * Returns true if this callback must be used only on client side.
		 */
		public boolean isClientCallback(){
			return this._isClientCallback;
		}
		
		/**
		 * Returns the name of the callback as defined in the
		 * db4o specifications.
		 * @return
		 */
		public String getName(){
			return this._name;
		}
	}

	/**
	 * Contains the list of standard templates used by {@link XmlSchemaUtils}.
	 * @author liviug
	 */
	private enum Templates implements TemplateList{
		CONFIGREGISTEREXTERNALCALLBACK(Templates.path, "configRegisterExternalCallback.vm"), 
		INITIALAPPLYCALLBACKMETHODCONTENT(Templates.path, "initialApplyCallbackMethodContent.vm"),
		INITIALCALLBACKMETHODCONTENT(Templates.path, "initialCallbackMethodContent.vm");
		private static final String path = "standardPlugins/java/maingenerator/xmlSchemaUtils";

		
		private String _path;
		private String _name;

		Templates(String path, String name){
			this._path = path;
			this._name = name;
		}
		
		/**
		 * Returns the path and the name of the template.
		 */
		public String getQualifiedName() {
			return this._path+"/"+this._name;
		}
	}
	
	
	public XmlSchemaUtils(MainGenerator mainGenerator) throws OdbgenException{
		this._mainGenerator = mainGenerator;
		this.initialize();
	}
	
	/**
	 * Returns the Db4oUtils class.
	 * See {@link XmlSchemaUtils} for more information.
	 * @return
	 */
	public TSClass getDb4oUtils(){
		return this._odb4UtilsClass;
	}
	

	/**
	 * Returns one of the 'applyConfig' methods. 
	 * See {@link XmlSchemaUtils} for more info on 'applyConfig' methods.
	 * @param scenario The config file to retrieve.
	 * @return
	 * @throws OdbgenException
	 */
	public TSMethod getApplyConfig(Scenario scenario) throws OdbgenException{
		if(scenario==XmlSchemaUtils.Scenario.CLIENT){
			return this._applyConfigClientMethod;
		}
		else if(scenario==XmlSchemaUtils.Scenario.SERVER){
			return this._applyConfigServerMethod;
		}
		else if(scenario==XmlSchemaUtils.Scenario.EMBEDDED){
			return this._applyConfigEmbeddedMethod;
		}
		else{
			throw new OdbgenException("Unknown config file type '%s'.", scenario.toString());
		}
	}

	/**
	 * Returns one of the 'applyCallbacks' methods. 
	 * See {@link XmlSchemaUtils} for more info on 'applyCallbacks' methods.
	 * @param scenario The config file to retrieve.
	 * @return
	 * @throws OdbgenException
	 */
	public TSMethod getApplyCallbacks(Scenario scenario) throws OdbgenException{
		if(scenario==XmlSchemaUtils.Scenario.CLIENT){
			return this._applyCallbacksClientMethod;
		}
		else if(scenario==XmlSchemaUtils.Scenario.SERVER){
			return this._applyCallbacksServerMethod;
		}
		else if(scenario==XmlSchemaUtils.Scenario.EMBEDDED){
			return this._applyCallbacksEmbeddedMethod;
		}
		else{
			throw new OdbgenException("Unknown config file type '%s'.", scenario.toString());
		}
	}

	/**
	 * Returns a standart callback identified by it's type and the config method it is part of.
	 * For more on config methods, see the documentatin for {@link XmlSchemaUtils}.
	 * @param callbackType The type of the callback.
	 * @param destination The config method the callback is called from.
	 * @return
	 * @throws OdbgenException
	 */
	public TSMethod getStandardCallback(
			XmlSchemaUtils.ExternalCallbackType callbackType, 
			XmlSchemaUtils.Scenario destination
			) throws OdbgenException{
		TSMethod standardCallback;

		//Verify if the callback is already generated.
		//If not, we have to generate the method, then return it.
		String methodName = this.getStandardCallbackMethodName(destination, callbackType);
		if(!this._standardCallbacks.containsKey(methodName)){
			standardCallback = this.addExternalCallback(
					callbackType,
					destination,
					methodName
					);
			this._standardCallbacks.put(methodName, standardCallback);
		}
		
		return this._standardCallbacks.get(methodName);
	}
	
	/**
	 * Creates all cllasses and methods as stated inside {@link XmlSchemaUtils} specification.
	 * @throws OdbgenException 
	 */
	private void initialize() throws OdbgenException{
		//Add Odb4Utils class.
		this._odb4UtilsClass = new TSClass(
				null,
				this._mainGenerator.getRootPackage(),
				TSModifier.getPublicModifier(),
				this.odb4UtilsClassName ,
				new ArrayList<TSField>(),
				null,
				new ArrayList<TSInterface>()
				);
		this._odb4UtilsClass.getImports().add("com.db4o.config.*");
		this._odb4UtilsClass.getImports().add("com.db4o.*");
		this._odb4UtilsClass.getImports().add("com.db4o.events.*");
		this._odb4UtilsClass.getImports().add("com.db4o.constraints.*");
		
		this._mainGenerator.getTypedXmlSchema().getTypeDeclarations().add(this._odb4UtilsClass);

		//Add applyConfigClient().
		List<TSMethodParameter> parameters = new ArrayList<TSMethodParameter>();
		parameters.add(new TSMethodParameter(
				"config",
				new DataType("", "Configuration", new ArrayList<DataType>())
				));

		//Build the content
		String content = "";
		
		this._applyConfigClientMethod = new TSMethod(
				false,
				TSModifier.getPublicStaticModifier(),
				new DataType("", PrimitiveType.VOID.getName(), new ArrayList<DataType>()),
				XmlSchemaUtils.Scenario.CLIENT.getApplyConfigMethodName(),
				parameters,
				content //Initially there is almost no content. Plugins may add content here.
		); 
		this._odb4UtilsClass.getMethods().add(this._applyConfigClientMethod);
		

		//Add applyConfigServer().
		parameters = new ArrayList<TSMethodParameter>();
		parameters.add(new TSMethodParameter(
				"config",
				new DataType("", "Configuration", new ArrayList<DataType>())
				));
		
		//Build the content
		content = "";

		this._applyConfigServerMethod = new TSMethod(
				false,
				TSModifier.getPublicStaticModifier(),
				new DataType("", PrimitiveType.VOID.getName(), new ArrayList<DataType>()),
				XmlSchemaUtils.Scenario.SERVER.getApplyConfigMethodName(),
				parameters,
				content //Initially there is almost no content. Plugins may add content here.
		); 
		this._odb4UtilsClass.getMethods().add(this._applyConfigServerMethod);


		//Add applyConfigEmbedded().
		parameters = new ArrayList<TSMethodParameter>();
		parameters.add(new TSMethodParameter(
				"config",
				new DataType("", "Configuration", new ArrayList<DataType>())
				));
		
		//Build the content
		content = "";

		this._applyConfigEmbeddedMethod = new TSMethod(
				false,
				TSModifier.getPublicStaticModifier(),
				new DataType("", PrimitiveType.VOID.getName(), new ArrayList<DataType>()),
				XmlSchemaUtils.Scenario.EMBEDDED.getApplyConfigMethodName(),
				parameters,
				content //Initially there is almost no content. Plugins may add content here.
		); 
		this._odb4UtilsClass.getMethods().add(this._applyConfigEmbeddedMethod);
		
		
		//Add applyCallbacksClient().
		parameters = new ArrayList<TSMethodParameter>();
		parameters.add(new TSMethodParameter(
				"container",
				new DataType("", "ObjectContainer", new ArrayList<DataType>())
				));

		//Build the content
		content = "";
		
		this._applyCallbacksClientMethod = new TSMethod(
				false,
				TSModifier.getPublicStaticModifier(),
				new DataType("", PrimitiveType.VOID.getName(), new ArrayList<DataType>()),
				XmlSchemaUtils.Scenario.CLIENT.getApplyCallbacksMethodName(),
				parameters,
				content //Initially there is almost no content. Plugins may add content here.
		); 
		this._odb4UtilsClass.getMethods().add(this._applyCallbacksClientMethod);

		//Add applyCallbacksServer().
		parameters = new ArrayList<TSMethodParameter>();
		parameters.add(new TSMethodParameter(
				"server",
				new DataType("", "ObjectServer", new ArrayList<DataType>())
				));
		
		//Build the content
		Template template = TemplatesUtils.getTemplateJar(XmlSchemaUtils.Templates.INITIALAPPLYCALLBACKMETHODCONTENT.getQualifiedName(), this._mainGenerator.getStandardTemplatesJarName());
		VelocityContext context = new VelocityContext();
		context.put("scenario", Scenario.SERVER.toString());
		content = TemplatesUtils.merge(template, context);

		this._applyCallbacksServerMethod = new TSMethod(
				false,
				TSModifier.getPublicStaticModifier(),
				new DataType("", PrimitiveType.VOID.getName(), new ArrayList<DataType>()),
				XmlSchemaUtils.Scenario.SERVER.getApplyCallbacksMethodName(),
				parameters,
				content //Initially there is almost no content. Plugins may add content here.
		); 
		this._odb4UtilsClass.getMethods().add(this._applyCallbacksServerMethod);
		
		
		//Add applyCallbacksEmbedded().
		parameters = new ArrayList<TSMethodParameter>();
		parameters.add(new TSMethodParameter(
				"container",
				new DataType("", "ObjectContainer", new ArrayList<DataType>())
				));
		
		//Build the content
		content = "";

		this._applyCallbacksEmbeddedMethod = new TSMethod(
				false,
				TSModifier.getPublicStaticModifier(),
				new DataType("", PrimitiveType.VOID.getName(), new ArrayList<DataType>()),
				XmlSchemaUtils.Scenario.EMBEDDED.getApplyCallbacksMethodName(),
				parameters,
				content //Initially there is almost no content. Plugins may add content here.
		); 
		this._odb4UtilsClass.getMethods().add(this._applyCallbacksEmbeddedMethod);

		
	}

	
	/**
	 * Returns the name of the standard external callback identified by
	 * the config method it is called from and the callback type.
	 * @param destination The config method the callback is called from.
	 * @param callbackType The callback type.
	 * @return
	 */
	private String getStandardCallbackMethodName(
			XmlSchemaUtils.Scenario destination,
			XmlSchemaUtils.ExternalCallbackType callbackType){
		return String.format("onStd%s%s",
				StringUtils.buildUppercaseName(callbackType.getName()),
				StringUtils.buildUppercaseName(destination.getApplyCallbacksMethodName())
				);
	}

	
	
	/**
	 * Used to add an external callback to one of the three confi methods.
	 * @param callbackType The type of the callback.
	 * @param destinationApplyCallbacksMethod Decides what config method to add the callback into.
	 * @param calllbackMethodName The name of the method representing the callback. Use this to later get the
	 * method and populate its content.
	 * @return Returns the newly added method representing the callback.
	 * @throws OdbgenException
	 */
	public TSMethod addExternalCallback(
			ExternalCallbackType callbackType,
			Scenario destinationApplyCallbacksMethod,
			String calllbackMethodName
			) throws OdbgenException{

		//Add the callback register code in config method.
		Template template = TemplatesUtils.getTemplateJar(XmlSchemaUtils.Templates.CONFIGREGISTEREXTERNALCALLBACK.getQualifiedName(), this._mainGenerator.getStandardTemplatesJarName());
		VelocityContext context = new VelocityContext();
		context.put("callbackName", callbackType.getName());
		context.put("callbackMethodName", calllbackMethodName);
		String registerCallbackContent = TemplatesUtils.merge(template, context);
		TSMethod configMethod;
		
		configMethod = this.getApplyCallbacks(destinationApplyCallbacksMethod);
		configMethod.getContent().append(registerCallbackContent);
		
		//Add the method corresponding to the callback.
		List<TSMethodParameter> parameters = new ArrayList<TSMethodParameter>();
		parameters.add(new TSMethodParameter(
				"e",
				new DataType("", "Event4", new ArrayList<DataType>())
				));
		parameters.add(new TSMethodParameter(
				"args",
				new DataType("", "EventArgs", new ArrayList<DataType>())
				));
		TSMethod callbackMethod = new TSMethod(
				false,
				TSModifier.getPublicStaticModifier(),
				new DataType("", PrimitiveType.VOID.getName(), new ArrayList<DataType>()),
				calllbackMethodName,
				parameters,
				this.getInitialCallbackMethodContent(callbackType)
				);
		this.getDb4oUtils().getMethods().add(callbackMethod);
		return callbackMethod;
	}

	
	/**
	 * Each callback method receive an 'EventArgs' argument. This has to be cast
	 * to a specific type.
	 * This is the initial content of a callback method.
	 * @param callbackType
	 * @return
	 * @throws OdbgenException 
	 */
	private String getInitialCallbackMethodContent(ExternalCallbackType callbackType) throws OdbgenException {

		//Get the real EventArgs object used by that callback.
		String realEventArgs;
		if(
				callbackType == ExternalCallbackType.QUERYFINISHED || 
				callbackType == ExternalCallbackType.QUERYSTARTED
			){
			realEventArgs = "QueryEventArgs";
		}
		else if(
				callbackType == ExternalCallbackType.CREATED || 
				callbackType == ExternalCallbackType.ACTIVATED || 
				callbackType == ExternalCallbackType.DEACTIVATED || 
				callbackType == ExternalCallbackType.UPDATED || 
				callbackType == ExternalCallbackType.DELETED 
			){
			realEventArgs = "ObjectEventArgs";
		}
		else if(
				callbackType == ExternalCallbackType.CREATING || 
				callbackType == ExternalCallbackType.ACTIVATING || 
				callbackType == ExternalCallbackType.DEACTIVATING || 
				callbackType == ExternalCallbackType.DELETING || 
				callbackType == ExternalCallbackType.UPDATING 
			){
			realEventArgs = "CancellableObjectEventArgs";
		}
		else if(
				callbackType == ExternalCallbackType.COMMITTING 
			){
			realEventArgs = "CommitEventArgs";
		}
		else if(
				callbackType == ExternalCallbackType.COMMITTED 
			){
			realEventArgs = "CommitEventArgs";
		}
		else{
			throw new OdbgenException("Unknown external callback type '%s'.", callbackType.getName());
		}

		//Build the initial content.
		Template template = TemplatesUtils.getTemplateJar(XmlSchemaUtils.Templates.INITIALCALLBACKMETHODCONTENT.getQualifiedName(), this._mainGenerator.getStandardTemplatesJarName());
		VelocityContext context = new VelocityContext();
		context.put("realEventArgs", realEventArgs);
		String res = TemplatesUtils.merge(template, context);
		
		return res;
	}

}
